package org.freedoit.common.util;

import it.sauronsoftware.base64.Base64;

import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPublicKeySpec;
import java.text.DateFormat;
import java.util.Map;
import java.util.Random;
import java.util.logging.Logger;

import javax.crypto.Cipher;

import org.bouncycastle.crypto.BufferedBlockCipher;
import org.bouncycastle.crypto.DataLengthException;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.engines.AESFastEngine;
import org.bouncycastle.crypto.modes.CBCBlockCipher;
import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithIV;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class FreeDoitDataUtils {

	private static final Logger log = Logger.getLogger(FreeDoitDataUtils.class.getName());
	private static final String ENCODE_NAME = "UTF-8";
//	private static final String AES_ENCRYPT_METHOD = "AES";
	private static final int AES_KEY_BIT_SIZE = 128;
	private static final int AES_KEY_BYTE_SIZE = AES_KEY_BIT_SIZE / 8;
    private static char hexDigits[] = {       //
        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D',  'E', 'F'};
    private static char keySource[] = {       //
        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D',  'E', 'F',
        'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',  'U', 'V',
        'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',  'k', 'l',
        'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',  '=', '-'};

    public static final Long NO_TIMESTAMP_KEY = new Long(-1);
    
    private static final int ONE_WAY_ENCRYPT_PLUS_LEN =8;
    private static final int RSA_KEY_SIZE = 1024;
    
    public static String encodeString(String plainText) {
    	if (plainText == null) {
    		return null;
    	} else {
    		return Base64.encode(plainText, ENCODE_NAME);
    	}
    }
    
    public static String decodeString(String plainText) {
    	if (plainText == null) {
    		return null;
    	} else {
    		return Base64.decode(plainText, ENCODE_NAME);
    	}
    }

	private static String hexString(byte[] source) {
	    char str[] = new char[16 * 2];
	    //
	    int k = 0;                                //
	    for (int i = 0; i < 16; i++) {          //
	        byte byte0 = source[i];                 // 
	        str[k++] = hexDigits[byte0 >>> 4 & 0xf];  // 
	        //  
	        str[k++] = hexDigits[byte0 & 0xf];            // 
	    }  
	    return new String(str);  
	}
	
	public static String hashString(String plainText) {
		return getMD5(plainText);
	}

	private static String getMD5(String plainText) {
		MessageDigest md5 = null;
		try {
			md5 = MessageDigest.getInstance("MD5");
		} catch (NoSuchAlgorithmException e) {
			// TODO Auto-generated catch block
			log.severe(e.getMessage());
			return null;
		}
	    try {
	        md5.update(plainText.getBytes(ENCODE_NAME));
	    } catch (UnsupportedEncodingException e) {
			log.severe(e.getMessage());
			return null;
	    }
		byte[] resultBytes= md5.digest();
		return hexString(resultBytes);
	}
	
	public static String buildJDBCURL(String dbType, String host, String port, String dbName) {		
		return buildJDBCURL(dbType, host, port, dbName, null);
	}
	
	public static String buildJDBCURL(String dbType, String host, String port, String dbName, 
			Map<String, String> params) {
		StringBuffer sbURL = new StringBuffer();
		
		sbURL.append("jdbc:").append(dbType).append("://");
		sbURL.append(host).append(":").append(port);
		sbURL.append("/").append(dbName);
		
		// Append parameters
		if (params != null && !params.isEmpty()) {
			sbURL.append("?");
			for (Map.Entry<String, String> paramEntry : params.entrySet()) {
				sbURL.append(paramEntry.getKey()).append("=");
				sbURL.append(paramEntry.getValue()).append("&");
			}
			
			// remove last "&"
			sbURL.deleteCharAt(sbURL.length() - 1);
		}
		
		return sbURL.toString();
	}
    
    public static Gson createStandardJson() {

        return new GsonBuilder()
            .registerTypeAdapter(java.sql.Date.class, new SqlDateTypeAdapter())
            .registerTypeAdapter(java.sql.Time.class, new SqlTimeTypeAdapter())
            .registerTypeAdapter(java.sql.Timestamp.class, new SqlTimestampTypeAdapter())
            .setDateFormat(DateFormat.LONG, DateFormat.LONG).create();
    }
    
    public static String oneWayEncryptPassword(String password) {
    	if (password == null || "".equals(password)) {
    		throw new IllegalArgumentException("Input password cannot be empty");
    	}
    	
    	int iPasswordLen = password.length();
    	
    	// Random retrieve char from password as seed
    	Random r = new Random();
    	
    	StringBuffer sbSeed = new StringBuffer();
    	StringBuffer sbRet = new StringBuffer();
    	
    	for (int i = 0; i < ONE_WAY_ENCRYPT_PLUS_LEN;  i ++) {
    		sbSeed.append(password.charAt(r.nextInt(iPasswordLen)));
    	}

    	// Insert the seed before password
    	// And generated the MD5 of this combined String
    	// Then Insert the seed before generated MD5
    	sbRet.append(sbSeed.toString());
    	sbSeed.append(password);
    	
    	sbRet.append(getMD5(sbSeed.toString()));
    	
    	return sbRet.toString();
    }
    
    public static boolean validateEncryptedPassword(String passwordEncrypted, 
    		String passwordOriginal) {
    	if (passwordEncrypted == null || "".equals(passwordEncrypted) ||
    			passwordEncrypted.length() <= ONE_WAY_ENCRYPT_PLUS_LEN) {
    		return false;
    	}
    	
    	String seed = passwordEncrypted.substring(0, ONE_WAY_ENCRYPT_PLUS_LEN);
    	String keyToAuth = passwordEncrypted.substring(ONE_WAY_ENCRYPT_PLUS_LEN);
    	String passwordOriginalEncrypted = getMD5(seed + passwordOriginal);
    	
    	return passwordOriginalEncrypted.equals(keyToAuth);
    }
    
    public static String generateRandomAESKey() {
        int iKeySourceSize = keySource.length;
        
        Random r = new Random();
        StringBuilder sbKeyRet = new StringBuilder();
        for (int i = 0; i < AES_KEY_BYTE_SIZE; i ++) {
        	sbKeyRet.append(keySource[r.nextInt(iKeySourceSize)]);
        }
        
        return sbKeyRet.toString();
        
    }
    
    private static byte[] getValidKey(String password, int keySize) {
    	if (password == null || "".equals(password)) {
    		throw new IllegalArgumentException("Password cannot be empty");
    	}
    	if (keySize <= 0) {
    		throw new IllegalArgumentException("Key Size must be positive integer");
    	}

    	int iTemp = keySize - password.length();
    	String strKey = password;
    	if (iTemp > 0) {
    		StringBuilder sb = new StringBuilder(password);
    		for (int i = 0; i < iTemp; i ++) { // fill with space
    			sb.append(" ");
    		}
    		
    		strKey = sb.toString();
    	}
    	
    	try {
			byte[] ret = new byte[keySize];
			System.arraycopy(strKey.getBytes(ENCODE_NAME), 0, ret, 0, keySize);
			return ret;
		} catch (UnsupportedEncodingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
    	
    	return null;
    }
    
    private static byte[] doAESCipher(byte[] dataIn, int mode, String password) {
        byte[] key = getValidKey(password, AES_KEY_BYTE_SIZE);
        if (key == null) {
        	return null;
        }

    	BufferedBlockCipher engine = new PaddedBufferedBlockCipher(
    			new CBCBlockCipher(new AESFastEngine()));

    	// We need to do this check, otherwise a DataLengthException
    	// will be thrown form BouncyCastle API, a bit weird
    	int retLenPlus = 0;
    	int iBlockSize = engine.getBlockSize();
    	if (dataIn.length % iBlockSize == 0) {
    		retLenPlus = iBlockSize;
    	}

        byte[] ret = new byte[engine.getOutputSize(dataIn.length) + retLenPlus];
        try {
            System.out.println("Key:" + new String(Base64.encode(key), "ASCII"));
            engine.init((mode == Cipher.ENCRYPT_MODE), new ParametersWithIV(
            		new KeyParameter(key), key));
            int olen = engine.processBytes(dataIn, 0, dataIn.length, ret, 0);
            olen += engine.doFinal(ret, olen);
            
            if (olen < ret.length) {
            	byte[] tmp = new byte[olen];
                System.arraycopy(ret, 0, tmp, 0, olen);
                ret = tmp;
            }
			return ret;
		} catch (DataLengthException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IllegalStateException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (InvalidCipherTextException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IllegalArgumentException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (UnsupportedEncodingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} 
	    return null;
    }
    
//    private static byte[] doAESCipher2(byte[] dataIn, int mode, String password) {
//        byte[] key = getValidKey(password, AES_KEY_BYTE_SIZE);
//        if (key == null) {
//        	return null;
//        }
//
//    	BouncyCastleProvider provider = new BouncyCastleProvider();
//        try {
//            System.out.println("Key:" + new String(Base64.encode(key), "ASCII"));
//            SecretKeySpec keySpec = new SecretKeySpec(key, AES_ENCRYPT_METHOD);
//            Cipher cipher = Cipher.getInstance(AES_ENCRYPT_METHOD, provider);//
//            cipher.init(mode, keySpec);//
//            
//            return cipher.doFinal(dataIn);
//	    } catch (NoSuchAlgorithmException e) {  
//	            e.printStackTrace();
//	    } catch (NoSuchPaddingException e) {  
//	            e.printStackTrace();
//	    } catch (InvalidKeyException e) {  
//	            e.printStackTrace();
//	    } catch (IllegalBlockSizeException e) {  
//	            e.printStackTrace();
//	    } catch (BadPaddingException e) {  
//	            e.printStackTrace();
//	    } catch (UnsupportedEncodingException e) {
//			// TODO Auto-generated catch block
//			e.printStackTrace();
//		}
//	    return null;
//    }

    public static String encryptByAES(String plainText, String password) {
    	System.out.println("Encrypt [" + plainText + "] by " + password);
        try {
        	return encryptByteByAES(plainText.getBytes(ENCODE_NAME), password);
	    } catch (UnsupportedEncodingException e) {  
	            e.printStackTrace();  
	    } 
	    return null;
    }
    
    public static String decryptByAES(String content, String password) {
    	System.out.println("Decrypt [" + content + "] by " + password);
        try {
            byte[] byteContent = decryptStringByAES(content, password);
            
            if (byteContent != null) {
            	return new String(byteContent, ENCODE_NAME);
            }
	    } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
	    }
	    return null;
	}

    private static String encryptByteByAES(byte[] byteDataIn, String password) {
        try {
            byte[] byteContent = doAESCipher(byteDataIn, 
            		Cipher.ENCRYPT_MODE, password);
            
            if (byteContent != null) {
                return new String(Base64.encode(byteContent), "ASCII");
            }
	    } catch (UnsupportedEncodingException e) {  
	            e.printStackTrace();  
	    } 
	    return null;
    }
    
    private static byte[] decryptStringByAES(String content, String password) {  
        try {
        	return doAESCipher(Base64.decode(content.getBytes("ASCII")), 
            		Cipher.DECRYPT_MODE, password);
	    } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
	    }
	    return null;
	}

    public static KeyPair generateKeyPair() {
        try {
            //
            KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA", new BouncyCastleProvider());
            //
            keyPairGen.initialize(RSA_KEY_SIZE, new SecureRandom());
            KeyPair keyPair = keyPairGen.genKeyPair();
            return keyPair;
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        return null;
    }
    
    public static RSAPublicKey generateRSAPublicKey(BigInteger modulus, BigInteger publicExponent) {
		KeyFactory keyFac = null;
		try {
		//
			keyFac = KeyFactory.getInstance("RSA", new BouncyCastleProvider());
		} catch (NoSuchAlgorithmException ex) {
			ex.printStackTrace();
		}
		
		if (keyFac == null)
			return null;

		//
		RSAPublicKeySpec pubKeySpec = new RSAPublicKeySpec(modulus, publicExponent);
		try {
		//
			return (RSAPublicKey) keyFac.generatePublic(pubKeySpec);
		} catch (InvalidKeySpecException ex) {
			ex.printStackTrace();
		}
		
		return null;
    }

    public static byte[] encryptByRSA(Key key, byte[] data) throws Exception {
        try {
            Cipher cipher = Cipher.getInstance("RSA", new BouncyCastleProvider());
            cipher.init(Cipher.ENCRYPT_MODE, key);
            //
            int blockSize = cipher.getBlockSize();
            //
            int outputSize = cipher.getOutputSize(data.length);
            int leavedSize = data.length % blockSize;
            //
            int blocksSize = leavedSize != 0 ? data.length / blockSize + 1 : data.length
                                                                             / blockSize;
            byte[] raw = new byte[outputSize * blocksSize];
            int i = 0;
            while (data.length - i * blockSize > 0) {
                if (data.length - i * blockSize > blockSize)
                    cipher.doFinal(data, i * blockSize, blockSize, raw, i * outputSize);
                else
                    cipher.doFinal(data, i * blockSize, data.length - i * blockSize, raw,
                        i * outputSize);
                 i++;
            }
            return raw;
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        return null;
    }

    public static byte[] decryptByRSA(Key key, byte[] raw) throws Exception {
        try {
            Cipher cipher = Cipher.getInstance("RSA", new BouncyCastleProvider());
            cipher.init(Cipher.DECRYPT_MODE, key);
            int blockSize = cipher.getBlockSize();
            ByteArrayOutputStream bout = new ByteArrayOutputStream(64);
            int j = 0;
            while (raw.length - j * blockSize > 0) {
                bout.write(cipher.doFinal(raw, j * blockSize, blockSize));
                j++;
            }
            return bout.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        return null;
    }

    public static String encryptByRSA(Key key, String data) {
        try {
            return new String(Base64.encode(encryptByRSA(key, data.getBytes(ENCODE_NAME))), "ASCII");
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        return null;
    }

    public static String decryptByRSA(Key key, String data) {
        try {
            return new String(decryptByRSA(key, Base64.decode(data.getBytes("ASCII"))), ENCODE_NAME);
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        return null;
    }
    
    public static void main(String[] args) throws Exception {
    	String entext = FreeDoitDataUtils.encryptByAES("{\"timestamp\":-1}{\"timestamp\":-1}", "23E6290952FCD8275F7B3F0721E04A2F");
//    	//String detext = FreeDoitDataUtils.decryptByAES("CmP6b9AzmihSOa5UTiBXlxDoEAlMGGpktgc2xS93MyrGULQCd452pPGRViN6Gh5OKCmdf0q3MpXeL8YZiO+5Es9qBZ6k88v+gGy/rq4BUuniEfYBA7BIaS9R/r6FjMogk8NctKVd2+QY2qEDewbTSyzq6z9wzNGdy/W9bYX/Xo7jqcx5z5Z5kgkVw42HLO7GdOIxtQiB+qd5JRiJwErlE0nz0pvKtF8XUVgfIpvVN52YHXBwUgovia3lXMDEUY/G9h8JYy/QkpYiqb4+VlRc91cJnOHvo7Zp1U10KFKE7AAQJscclRb8F1wVE20nOvUGq9SPuVkKY78X7EBw9yWTAx8Uk38UYpYi9WEDgKRz3BLoaADAG7rRDRS+1GM9CKsMgarG9nyW9nj83eDIQS/WNQLUlfcg/fNBw6d+7FKRxQs=", 
//    			"23E6290952FCD8275F7B3F0721E04A2F");
    	
    	System.out.println(entext);
    	System.out.println(FreeDoitDataUtils.decryptByAES(entext, "23E6290952FCD8275F7B3F0721E04A2F"));
    }
    
}
